feat(Android, Stack v5): handle header configuration and custom subviews#3796
Merged
feat(Android, Stack v5): handle header configuration and custom subviews#3796
Conversation
It doesn't use any CoordinatorLayout features.
I'm not really sure which context should we use. Might want to revisit it later.
TODO: handle layout of CoordinatorLayout, now it's added in a random place.
I did not apply any changes to color - header won't be transparent but will have correct layout for transparent header.
Base automatically changed from
@kligarski/stack-v5-android-header-skeleton
to
main
April 9, 2026 17:05
kmichalikk
approved these changes
Apr 15, 2026
t0maboro
approved these changes
Apr 20, 2026
…ementation detail
kmichalikk
approved these changes
Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds support for header configuration and providing custom subviews for M3 App Bar in Stack v5.
Closes https://github.com/software-mansion/react-native-screens-labs/issues/916.
Details
JS API
After some discussions we decided that the API for header configuration will look as follows:
Stackobject will provide aStack.HeaderConfigcomponent only.Stack.HeaderConfigcomponent.Stack.HeaderConfigwill be split in similar way toTabsHostandTabsScreenbetween platforms:title,hidden),android/iosprop.StackHeaderSubviewwill be Android-only).High-level flow of updates
Header attachment
StackHeaderConfigis attached toStackScreen.StackHeaderCoordinatorLayoutcreatesStackHeaderCoordinatorinstance.CLattaches its ownonHeaderHeightChangedcallback to the coordinator. It will be called whenAppBarLayoutis scrolled and will propagate correct Y offset toStackScreen.StackHeaderCoordinatorLayoutattaches its own callback asonHeaderConfigAttachListeneronStackScreen(weak ref) and ifstackScreen.headerConfigis available, it uses the config.StackHeaderCoordinatorLayout.handleHeaderConfigAttachruns and attaches its own callback asonHeaderConfigChangeListeneron new config (weak ref). This callback passes all updates toheaderCoordinatorviaapplyHeaderConfig(it also batches the updates). Initial update is performed.Subview attachment
StackHeaderSubviewis attached toStackHeaderConfig.StackHeaderSubviewattaches itself asonStackHeaderSubviewChangeListener(weak ref) to the subview (to informHeaderCoordinatorabout e.g.collapseModeprop change on the subview).HeaderConfiginformsHeaderCoordinatorviaonConfigChangeListenerabout the new subview.Offset shadow tree update
AppBarLayout.ScrollingViewBehavior.onDependentViewChangedruns.StackHeaderScrollingViewBehaviorto allowheaderCoordinatorto attach its own callback.StackScreen's offset viaonHeaderHeightChangedcallback.AppBarLayout(so that it works with both non-transparent and transparent header). They update size & offset forHeaderConfigand offsets forHeaderSubviews viasyncShadowStatemethod.Scrolling behaviors
M3 App Bar suppors various scrolling behaviors. In order to use them, we need to use
ScrollingViewBehaviorviaCoordinatorLayout. Important thing to note is that the content below the header has constant size. Scrolling the content scrolls the app bar and the content. The size of the content is calculated byCoordinatorLayoutso that the content is fully visible when header is collapsed. When header is expanded, some part of the screen is cut off.Please also note that for this to work we need to use
NestedScrollViewor at least regularScrollViewwithnestedScrollEnabled={true}."Transparent" header
The intention of this PR is to focus only on layout-related props, not the final API. I added
transparentthat changes the layout but not the background color.Another thing to consider is whether
transparentprop should exist or should it be layout-related only & background color can be handled via regular stylingbackgroundColorprop. This might be even more useful for iOS implementation of the header but it needs to be researched (cc @kmichalikk)."Transparent" header does not make sense with scrolling behaviors because the content will never be rendered below the header. That's why in this mode the screen fills the entire container and does not move.
Custom subviews sizing
In Stack v4 we faced many problems with handling custom subviews. For Stack v5 we decided that the size of
leading(leftin LTR,rightin RTL),centerandtrailing(rightin LTR,leftin RTL) subviews will be determined fully by Yoga and it must depend on the size of the content inside of the subview. Subviews will not be aware of the position and size of other subviews and flex behaviors will not work (but obviously if you wrap subview content in a view with specificwidthandheight, inside the wrapperflexwill work as usual). The layout of the subview (position in the toolbar) will be determined fully by the native layout.This simplified approach will allow us to reliably layout the subviews without Yoga and native layout fighting each other.
Stack v5 will additionally support
backgroundsubview for collapsing headers (mediumandlarge). This view will be rendered as the background of the header. The subview will always be resized to the full size of theAppBarLayout(usingflexwill be supported).Layout synchronization between Yoga and native
Note
The background subview might have
collapseMode: 'parallax'effect applied - then it's position moves slower thanAppBarLayout. Light blue box represents background subview withcollapseMode: 'parallax'as this is the most complicated example, best for the explanation.Problems with custom subview ordering
Title in small header
When
smallheader is used, we would usually rely on title prop fromToolbar. There is however one caveat when using custom views - title is laid out before subviews therefore leading subview will always appear after the title. This in isolation might not be considered a problem (due to this being a native behavior) but inmedium/largecollapsing header, title is managed byCollapsingToolbarLayoutwhich doesn't use the title fromToolbarbut attaches its own customdummyViewas a subview ofToolbar-> in this configuration if you add a subview withGravity.START, it will be laid out before the title. We don't want the behavior to differ in such manner between header types so we decided that we should align this. It's easier to manage title insmallbehavior and leavemedium/largeas is (but it turns out that for RTL we need to do some hacky workarounds for collpsing header either way, details below).To ensure that title is laid out after the leading subview, we don't rely on
Toolbar.titlebut add our own view that tries to 1:1 mimic native title view.Ensuring correct subview order in RTL
For
smallheader, we need to make sure that leading subview and title view is added in correct order.For
medium/largeheader, the situation is more complicated.dummyViewis always added last, after our subviews. This causes incorrect ordering. In order to fix this, we need to make sure thatdummyViewis created and attached (by forcingmeasure). Then we manually change the order of the subviews (movingdummyViewto the index 0 fixes the problem).Known issues & future upgrades related to layout
Adding/removing subviews in runtime
The order of inserting subviews is critical for correct layout and it is very fragile. For now, when subviews are added/removed in runtime, we're recreating the hierarchy from scratch.
rebuild_subview.mp4
In the future we might research whether we can optimize this to prevent unnecessary rebuilds.
Background subview collapse mode
Currently,
backgroundsubview supports only 2 of 3 availablecollapseModes:offandparallax. As we wanted to allow usingflex: 1inside thebackgroundsubview, we need to set the size of theHeaderConfigcomponent & use absolute fill on the subview so that it matchesAppBarLayout. This however is problematic forpincollapse mode. If subview takes all the space, it behaves exactly likeoffcollapse mode - there is no additional space between the bottom edge of the header subview and bottom edge ofAppBarLayout/CollapsingToolbarLayoutso it scrolls immediately.In the future, we can consider exposing maunal size configuration for the subview (or finding another solution) to support
collapseMode: 'pin'.Maintaining scroll on rebuild
Currently, when header needs rebuilding, it doesn't preserve
AppBarLayout's scroll position and it expands automatically.rebuild_scroll.mp4
We should research whether we can cache the scroll offset between rebuilds.
hitSlophitSlopworks correctly now but in the context of native hierarchy. When in collapsing toolbar you click belowMaterialToolbareven though thehitSloprange would be enough, the touch won't be registered. Additionally, collapsing header usesdummyViewthat does not display any text when header is expanded but it will prevent touches which might be surprising for users.We should consider supporting
hitSlopin fullHeaderConfigarea.Margins for header items
Margins between subviews are inconsistent due to differences in layout.
smalllargeWe should consider exposing full margin configuration for subviews (it's a little bit complicated with the margins used in native layout) and maybe provide more consistent defaults.
Scroll flags
Android allows full customization of scrolling behavior.
We need to expose the scroll flags in one of the follow-up PRs. They are hard-coded for now.
Handling insets
For now, we're relying on
fitsSystemWindowonAppBarLayoutto apply padding to avoid system bars and display cutout. This will be problematic for nested stack support, using insets from decor view in first render and maunal control (similar to #3835).In the future, we should manually apply the inset (but I remember from my research that this might be more difficult than it seems). If insets are read from decor, we might be able to use choreographer for
requestLayoutinStackContainerinstead ofpost(currently, using choreographer as in tabs causes layout jump on first render after rebuild).Title layout customization
We need to add support for centering the title & handle our managed title view in
smallheader.Navigation icon and menu
We need to add navigation icon (back button) and allow its customization. In the future we want to also expose Menu API.
RTL text ellipsize bug
There are some problems with text ellipsize in RTL when custom subviews are used.
rtl_ellipisze_bug.mp4
We should check whether this is a native bug or not.
Changes
JS
StackHeaderConfigcomponent with Android base implementation and skeleton for iOSStackHeaderSubviewcomponentreact-native.config.jsStackContainerto accept header configuration as a prop and render header config component if the prop is non-nullNative
detachFromCurrentParentViewextensionShadowStateProxyfromStackScreento allow re-use between componentsStackHeaderConfigandStackHeaderSubviewOnHeaderConfigAttachListener,OnHeaderConfigChangeListenerinterfacesrequestLayoutworksC++
StackHeaderConfigandStackHeaderSubviewHeaderSubviewVisual documentation
general.mp4
small_opaque.mp4
small_transparent.mp4
large_opaque.mp4
large_transparent.mp4
Medium is analogous to large, just differs in size.
Test plan
Run
test-stack-subviews.tsx. This test contains many props as subview layout is depended on many factors but some parts of the test should be extracted and tested separately (e.g. hidden, transparent, ...). With @LKuchno we decided that this SFT will be split and scenarios will be added in the future as some parts of the implementation might still change.Checklist